#!/usr/bin/env bash

set -o errexit
set -o pipefail

DC="${DC:-exec}"

# Disable tty allocation for Docker Compose when none is available, such as
# in CI or when you pipe something into the run script. 0 = stdin, 1 = stdout.
TTY="${TTY:-}"
if ! [ -t 0 ] || ! [ -t 1 ]; then
  TTY="-T"
fi

# -----------------------------------------------------------------------------
# Helper functions start with _ and aren't listed in this script's help menu.
# -----------------------------------------------------------------------------

_dc() {
  # shellcheck disable=SC2086
  docker compose "${DC}" ${TTY} "${@}"
}

_dc_run() {
  DC="run" _dc --no-deps --rm "${@}"
}

# -----------------------------------------------------------------------------

cmd() {
  # Run any command you want in the web container
  _dc web "${@}"
}

rails() {
  # Run any Rails commands
  cmd rails "${@}"
}

test() {
  # Run your Rails tests, use `test -b` to first rebuild your JS and CSS
  local run_build="${1:-}"
  local test_command="rails test"

  if [ "${run_build}" = "-b" ]; then
    test_command="yarn build && yarn build:css && ${test_command}"
    shift
  fi

  _dc -e "RAILS_ENV=test" js bash -c "${test_command} ${*:-test/}"
}

shell() {
  # Start a shell session in the web container
  cmd bash "${@}"
}

psql() {
  # Connect to PostgreSQL with psql
  # shellcheck disable=SC1091
  . .env
  _dc postgres psql -U "${POSTGRES_USER}" "${@}"
}

redis-cli() {
  # Connect to Redis with redis-cli
  _dc redis redis-cli "${@}"
}

lint:dockerfile() {
  # Lint Dockerfile
  docker container run --rm -i \
    -v "${PWD}/.hadolint.yaml:/.config/hadolint.yaml" \
    hadolint/hadolint hadolint "${@}" - <Dockerfile
}

lint:shell() {
  # Lint shell scripts
  local cmd=(shellcheck)

  if ! command -v shellcheck >/dev/null 2>&1; then
    local cmd=(docker container run --rm -i -v "${PWD}:/mnt" koalaman/shellcheck:stable)
  fi

  find . -type f \
    ! -path "./.git/*" \
    ! -path "./.ruff_cache/*" \
    ! -path "./app/*" \
    ! -path "./assets/*" \
    ! -path "./public/*" \
    ! -path "./storage/*" \
    ! -path "./tmp/*" \
    ! -path "./vendor/*" \
    -exec grep --quiet '^#!.*sh' {} \; -exec "${cmd[@]}" {} +
}

format:shell() {
  # Format shell scripts
  local cmd=(shfmt)

  if ! command -v shfmt >/dev/null 2>&1; then
    local cmd=(docker container run --rm -i -v "${PWD}:/mnt" -u "$(id -u):$(id -g)" -w /mnt mvdan/shfmt:v3)
  fi

  local maybe_write=("--write")

  for arg in "${@}"; do
    if [ "${arg}" == "-d" ] || [ "${arg}" == "--diff" ]; then
      unset "maybe_write[0]"
    fi
  done

  "${cmd[@]}" "${maybe_write[@]}" "${@}" .
}

format() {
  # Format Ruby code, optionally pass in --auto-correct to fix issues
  cmd rubocop "${@}"
}

quality() {
  # Perform all code quality commands together
  lint:dockerfile
  lint:shell

  format:shell
  format
}

deps:install() {
  local no_build="${1:-}"

  [ -z "${no_build}" ] && docker compose down && docker compose build

  _dc_run js yarn install
  _dc_run web bundle install
}

bundle() {
  cmd bundle "${@}"
}

bundle:outdated() {
  # List any installed gems that are outdated
  _dc_run web bundle outdated
}

bundle:update() {
  # Update any installed gems that are outdated
  _dc_run js bundle update
  deps:install "${@}"
}

yarn() {
  _dc js yarn "${@}"
}

yarn:outdated() {
  # Install yarn dependencies and write lock file
  _dc_run js yarn outdated
}

yarn:build() {
  # Build JS assets, this is only meant to be referenced from your package.json
  local args=()

  if [ "${NODE_ENV:-}" == "production" ]; then
    args=(--minify)
  elif [ "${RAILS_ENV:-}" == "development" ]; then
    args=(--sourcemap --watch)
  fi

  esbuild app/javascript/*.* --outdir=app/assets/builds --bundle "${args[@]}"
}

yarn:build:css() {
  # Build CSS assets, this is only meant to be referenced from your package.json
  local args=()

  if [ "${NODE_ENV:-}" == "production" ]; then
    args=(--minify)
  elif [ "${RAILS_ENV:-}" == "development" ]; then
    args=(--watch)
  fi

  tailwindcss \
    -i ./app/assets/stylesheets/application.tailwind.css \
    -o ./app/assets/builds/application.css "${args[@]}"
}

clean() {
  # Remove cache and other machine generates files
  rm -rf node_modules/ app/assets/builds/* public/assets tmp/* .byebug_history
}

ci:install-deps() {
  # Install Continuous Integration (CI) dependencies
  sudo apt-get install -y curl
  sudo curl \
    -L https://raw.githubusercontent.com/nickjj/wait-until/v0.1.2/wait-until \
    -o /usr/local/bin/wait-until && sudo chmod +x /usr/local/bin/wait-until
}

ci:test() {
  # Execute Continuous Integration (CI) pipeline
  lint:dockerfile "${@}"
  lint:shell
  format:shell --diff

  cp --no-clobber .env.example .env

  docker compose build
  docker compose up -d

  # shellcheck disable=SC1091
  . .env
  wait-until "docker compose exec -T \
    -e PGPASSWORD=${POSTGRES_PASSWORD} postgres \
    psql -U ${POSTGRES_USER} ${POSTGRES_USER} -c 'SELECT 1'"

  format -f github

  docker compose logs

  rails db:setup
  test -b
}

help() {
  printf "%s <task> [args]\n\nTasks:\n" "${0}"

  compgen -A function | grep -v "^_" | cat -n

  printf "\nExtended help:\n  Each task has comments for general usage\n"
}

# This idea is heavily inspired by: https://github.com/adriancooney/Taskfile
TIMEFORMAT=$'\nTask completed in %3lR'
time "${@:-help}"
